﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.Collections;
using System.Xml.Serialization;
using System.Xml;

namespace Argos.Data.Model
{
	/// <summary>
	/// A base class for entity item classes that make up the logical data model for Argos.
	/// </summary>
	[Serializable]
	public abstract class ItemBase : INotifyPropertyChanged , ICloneable
	{

		#region #ctor: protected ItemBase()

		/// <summary>
		/// Creates a new instance of the class.
		/// </summary>
		protected ItemBase()
		{
			this.Id = Guid.NewGuid();
		}

		#endregion


		/// <summary>
		/// Defines the XML namespace URI for elements in this namespace.
		/// </summary>
		public const string XmlNamespace = "http://schemas.develore.com/2008/06/argos/data/model.xsd";



		#region public event PropertyChangedEventHandler PropertyChanged

		/// <summary>
		/// Fired when the value of a property in the class has changed.
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged;

		#endregion



		#region public Guid Id { get; set; }

		private static string IdPropertyName = "Id";

		/// <summary>
		/// Sets or returns the ID of the item.
		/// </summary>
		[Browsable(false)]
		[XmlAttribute("id", Namespace = ItemBase.XmlNamespace)]
		public Guid Id
		{
			get { return this.GetProperty<Guid>(IdPropertyName); }
			set { this.SetProperty(IdPropertyName, value); }
		}

		#endregion		



		private Dictionary<string, object> Properties = new Dictionary<string, object>();



		#region public virtual object Clone()

		/// <summary>
		/// Clones the current instance.
		/// </summary>
		/// <remarks>
		/// This method requires that the current class is serializable.
		/// </remarks>
		public virtual object Clone()
		{
			BinaryFormatter f = new BinaryFormatter();
			using (MemoryStream strm = new MemoryStream())
			{
				f.Serialize(strm, this);
				strm.Position = 0;

				return f.Deserialize(strm);
			}
		}

		#endregion

		#region public override int GetHashCode()

		/// <summary>
		/// Returns the hash code for the object instance.
		/// </summary>
		public override int GetHashCode()
		{
			ArrayList list = new ArrayList(this.Properties.Keys);
			list.AddRange(this.Properties.Values);
			list.Add(this.Id);
			return list.ToArray().GetHashCode();
		}

		#endregion

		#region public virtual void InsertOnSubmit(Argos.Data.SqlServer.ArgosDatabase context)

		/// <summary>
		/// When overridden in a derived class, adds the item to the given data
		/// context to be inserted to the underlying SQL Server database when changes
		/// to the given context are submitted.
		/// </summary>
		/// <param name="context">The data context to insert the item to.</param>
		/// <exception cref="NotImplementedException">This implementation of the method always throws this exception.</exception>
		public virtual void InsertOnSubmit(Argos.Data.SqlServer.ArgosDatabase context)
		{
			throw new NotImplementedException("This method is not implemented.");
		}

		#endregion
		
		#region public byte[] ToByteArray()

		/// <summary>
		/// Serializes the object instance to a byte array.
		/// </summary>
		public byte[] ToByteArray()
		{
			byte[] buffer = null;
			using (MemoryStream strm = new MemoryStream())
			{
				this.Write(strm);
				strm.Position = 0;
				using (BinaryReader reader = new BinaryReader(strm))
				{
					buffer = reader.ReadBytes((int)strm.Length);
				}
			}

			return buffer;
		}

		#endregion

		#region public string ToXmlString()

		/// <summary>
		/// Returns the current object instance serialized to an XML string.
		/// </summary>
		public string ToXmlString()
		{
			using (MemoryStream strm = new MemoryStream())
			{
				this.WriteXml(strm);
				strm.Position = 0;

				using (StreamReader reader = new StreamReader(strm))
				{
					return reader.ReadToEnd();
				}
			}
		}

		#endregion
		
		#region public void Write(Stream stream)

		/// <summary>
		/// Serializes the current object instance and writes it to the given stream.
		/// </summary>
		/// <param name="stream">The stream to write the object instance to.</param>
		public void Write(Stream stream)
		{
			if (null == stream)
			{
				throw new ArgumentNullException("stream");
			}

			BinaryFormatter f = new BinaryFormatter();
			f.Serialize(stream, this);
		}

		#endregion

		#region public void Write(FileInfo targetFile)

		/// <summary>
		/// Serializes the current object instance and writes it to the given file.
		/// </summary>
		/// <param name="targetFile">The file to write the object instance to.</param>
		/// <remarks>
		/// If the file specified in <paramref name="targetFile"/> already exists, that file
		/// will be overwritten. If it does not exist, it will be created.
		/// </remarks>
		public void Write(FileInfo targetFile)
		{
			using (FileStream strm = new FileStream(targetFile.FullName, FileMode.Create, FileAccess.Write))
			{
				this.Write(strm);
			}
		}

		#endregion

		#region public void WriteXml(Stream stream)

		/// <summary>
		/// Serializes the current object instance to XML and writes it
		/// to the given stream.
		/// </summary>
		/// <param name="stream">The stream to write the serialized XML to.</param>
		public void WriteXml(Stream stream)
		{
			XmlSerializer ser = new XmlSerializer(this.GetType());
			ser.Serialize(stream, this);
		}

		#endregion

		#region public void WriteXml(FileInfo targetFile)

		/// <summary>
		/// Serializes the current object instance to XML and writes it
		/// to the given file.
		/// </summary>
		/// <param name="targetFile">The file to write the serialized XML to.</param>
		public void WriteXml(FileInfo targetFile)
		{
			using (FileStream strm = new FileStream(targetFile.FullName, FileMode.Create, FileAccess.Write))
			{
				this.WriteXml(strm);
			}
		}

		#endregion



		#region protected static T Read<T>(Stream stream)

		/// <summary>
		/// Deserializes the content in the given stream and returns it as the type
		/// specified in <typeparamref name="T"/>.
		/// </summary>
		/// <typeparam name="T">The type to return the deserialized object as.</typeparam>
		/// <param name="stream">The stream containing the serialized object.</param>
		protected static T Read<T>(Stream stream) where T : ItemBase
		{
			if (null == stream)
			{
				throw new ArgumentNullException("stream");
			}

			BinaryFormatter f = new BinaryFormatter();
			return (T)f.Deserialize(stream);
		}

		#endregion

		#region protected static T Read<T>(FileInfo sourceFile)

		/// <summary>
		/// Deserializes the content in the given file and returns it as the type
		/// specified in <typeparamref name="T"/>.
		/// </summary>
		/// <typeparam name="T">The type to return the deserialized object as.</typeparam>
		/// <param name="sourceFile">The file containing the serialized object.</param>
		protected static T Read<T>(FileInfo sourceFile) where T : ItemBase
		{
			using (FileStream strm = new FileStream(sourceFile.FullName, FileMode.Open, FileAccess.Read))
			{
				return Read<T>(strm);
			}
		}

		#endregion

		#region protected static T Read<T>(byte[] buffer)

		/// <summary>
		/// Deserializes the content in the given byte array and returns it as the type
		/// specified in <typeparamref name="T"/>.
		/// </summary>
		/// <typeparam name="T">The type to return the deserialized object as.</typeparam>
		/// <param name="buffer">The byte array containing the serialized object.</param>
		protected static T Read<T>(byte[] buffer) where T : ItemBase
		{
			if (null == buffer)
			{
				throw new ArgumentNullException("buffer");
			}

			using (MemoryStream strm = new MemoryStream())
			{
				strm.Write(buffer, 0, buffer.Length);
				strm.Flush();
				strm.Position = 0;

				return Read<T>(strm);
			}
		}

		#endregion

		#region protected static T ReadXml<T>(Stream stream)

		/// <summary>
		/// Reads the XML stored in the given stream and returns it as
		/// the given type.
		/// </summary>
		/// <typeparam name="T">The type to return the deserialized XML as.</typeparam>
		/// <param name="stream">The stream to read the XML from.</param>
		protected static T ReadXml<T>(Stream stream)
		{
			XmlSerializer ser = new XmlSerializer(typeof(T));
			return (T)ser.Deserialize(stream);
		}

		#endregion

		#region protected static T ReadXml<T>(FileInfo sourceFile)

		/// <summary>
		/// Deserializes the XML stored in the given file and returns it as
		/// the given type.
		/// </summary>
		/// <typeparam name="T">The type to return the deserialized XML as.</typeparam>
		/// <param name="sourceFile">The source file to read the XML from.</param>
		protected static T ReadXml<T>(FileInfo sourceFile)
		{
			using (FileStream strm = new FileStream(sourceFile.FullName, FileMode.Open, FileAccess.Read))
			{
				return ReadXml<T>(strm);
			}
		}

		#endregion



		#region protected T GetProperty<T>(string propertyName)

		/// <summary>
		/// Returns the value of the named property.
		/// </summary>
		/// <typeparam name="T">The type to return the value as.</typeparam>
		/// <param name="propertyName">The name of the property whose value to return.</param>
		/// <remarks>
		/// If the property does not exist, or the existing property cannot be
		/// casted to the type specified in <typeparamref name="T"/>, the default
		/// value for the given type is returned.
		/// </remarks>
		protected T GetProperty<T>(string propertyName)
		{
			return this.GetProperty<T>(propertyName, default(T));
		}

		#endregion

		#region protected T GetProperty<T>(string propertyName, T defaultValue)

		/// <summary>
		/// Returns the value of the named property.
		/// </summary>
		/// <typeparam name="T">The type to return the value as.</typeparam>
		/// <param name="propertyName">The name of the property whose value to return.</param>
		/// <param name="defaultValue">
		/// The default value to return in case the named property does not exist or its
		/// value cannot be casted to the type specified in <typeparamref name="T"/>.
		/// </param>
		protected T GetProperty<T>(string propertyName, T defaultValue)
		{
			if (this.Properties.ContainsKey(propertyName) && this.Properties[propertyName] is T)
			{
				return (T)this.Properties[propertyName];
			}

			return defaultValue;
		}

		#endregion

		#region protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)

		/// <summary>
		/// Fires the <see cref="PropertyChanged"/> event.
		/// </summary>
		/// <param name="e">The arguments for the event.</param>
		protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
		{
			if (null != this.PropertyChanged)
			{
				this.PropertyChanged.Invoke(this, e);
			}
		}

		#endregion

		#region protected void SetProperty(string propertyName, object propertyValue)

		/// <summary>
		/// Sets the value of the given property.
		/// </summary>
		/// <param name="propertyName">The name of the property whose value to set.</param>
		/// <param name="propertyValue">The value of the property.</param>
		protected void SetProperty(string propertyName, object propertyValue)
		{
			object val = this.GetProperty<object>(propertyName, null);
			if (!object.Equals(val, propertyValue))
			{
				this.Properties[propertyName] = propertyValue;
				this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
			}
		}

		#endregion

		#region protected T TryGetSerializedValue<T>(SerializationInfo info, string valueName)

		/// <summary>
		/// Tries to get the named value from the given serialization info class.
		/// </summary>
		/// <typeparam name="T">The type to return the value as.</typeparam>
		/// <param name="info">The class holding the value to try to get.</param>
		/// <param name="valueName">The name of the value to get.</param>
		/// <returns>
		/// Returns the value if it exists in <paramref name="info"/>. Returns the
		/// default value for the type <typeparamref name="T"/> otherwise.
		/// </returns>
		protected T TryGetSerializedValue<T>(SerializationInfo info, string valueName)
		{
			return this.TryGetSerializedValue<T>(info, valueName, default(T));
		}

		#endregion

		#region protected T TryGetSerializedValue<T>(SerializationInfo info, string valueName, T defaultValue)

		/// <summary>
		/// Tries to get the named value from the given serialization info class.
		/// </summary>
		/// <typeparam name="T">The type to return the value as.</typeparam>
		/// <param name="info">The class holding the value to try to get.</param>
		/// <param name="valueName">The name of the value to get.</param>
		/// <param name="defaultValue">
		/// The default value to return if the named value cannot be found or
		/// if it cannot be casted to the specified type.
		/// </param>
		/// <returns>
		/// Returns the value if it exists in <paramref name="info"/>. Returns the
		/// value specified in <see cref="defaultValue"/> otherwise.
		/// </returns>
		protected T TryGetSerializedValue<T>(SerializationInfo info, string valueName, T defaultValue)
		{
			try
			{
				object obj = info.GetValue(valueName, typeof(T));
				return (T)obj;
			}
			catch
			{
				return defaultValue;
			}
		}

		#endregion

	}
}
